﻿/* 
 * XBee.cs
 * 
 * Copyright (c) 2009, Michael Schwarz (http://www.schwarz-interactive.de)
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
 * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * 
 */
/*
 * MS	08-11-10	changed how data reading is working
 * BL   09-01-27    fixed MicroZigbee build
 * MS   09-02-06    fixed work item 3636 when first character is not the startbyte
 * PH   09-02-07    added several changes to enable stopping receive thread
 * MS   09-03-24    changed methods from SendCommand to Execute
 * MS   09-03-27    fixed if there are more than one API frame in received bytes
 *                  changed OnPacketReceived to FrameReceived
 *                  
 * 
 */
using System;
using System.IO;
using System.IO.Ports;
using System.Text;
using System.Threading;
using MFToolkit.IO;

namespace MFToolkit.Net.XBee
{
    /// <summary>
    /// Represents a XBee module communication class
    /// </summary>
    public class XBee : IDisposable
    {
        private MemoryStream _readBuffer = new MemoryStream();
        private ApiType _apiType;

        private byte _frameId;
        private bool _waitResponse;
        private XBeeResponse _receivedPacket;
        private readonly object _xBeeLock = new object();
        private readonly object _syncLock = new object();

        //private Thread _thd;
        //private bool _stopThd;

        #region Events

        public event FrameReceivedEventHandler FrameReceived;
        public event ModemStatusChangedEventHandler ModemStatusChanged;
        public event LogEventHandler LogEvent;

        #endregion

        #region Public Properties

        public ApiType ApiType
        {
            get { return _apiType; }
            protected set { _apiType = value; }
        }

        public SerialPort SerialPort { get; private set; }

        #endregion

        #region Constructor

        ~XBee()
        {
            Dispose();
        }

        public XBee(string port)
            : this(port, 9600)
        {
        }

        public XBee(string port, int baudRate)
            : this(port, baudRate, ApiType.Enabled)
        {
        }

        public XBee(string port, ApiType apiType)
            : this(port, 9600, apiType)
        {
        }

        public XBee(string port, int baudRate, ApiType apiType)
        {
            SerialPort = new SerialPort(port, baudRate) {ReadTimeout = 2000, WriteTimeout = 1000};
            _apiType = apiType;
            SerialPort.DataReceived += SerialPortDataReceived;
        }

        #endregion

        #region Public Methods

        public void Open()
        {
            SerialPort.Open();
        }

        /// <summary>
        /// Close the connection to the XBee module
        /// </summary>
        public void Close()
        {
            if (SerialPort != null)
                SerialPort.Close();
        }

        public bool IsOpen
        {
            get { return SerialPort.IsOpen; }
        }

        /// <summary>
        /// Sends a XBeeRequest
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public bool ExecuteNonQuery(XBeeRequest request)
        {
            if (request is XBeeFrameRequest)
            {
                if (_frameId == byte.MaxValue)
                    _frameId = byte.MinValue;

                ((XBeeFrameRequest) request).FrameID = ++_frameId;
            }

            byte[] bytes;

            switch (_apiType)
            {
                case ApiType.Disabled:
                    bytes = request.GetAtPacket();
                    break;
                case ApiType.Enabled:
                    bytes = request.GetApiPacket();
                    break;
                case ApiType.EnabledWithEscaped:
                    bytes = request.GetEscapedApiPacket();
                    break;
                default:
                    throw new NotSupportedException("This ApiType is not supported.");
            }

#if(LOG && !MF && !WindowsCE)
            Console.WriteLine(">>\t" + ByteUtil.PrintBytes(bytes, false));
#endif

            lock(_xBeeLock)
                SerialPort.Write(bytes, 0, bytes.Length);

            return true;
        }

#if(!MF && !WindowsCE)

        public T Execute<T>(XBeeRequest request) where T : XBeeResponse
        {
            return (T) Execute(request);
        }

        public T Execute<T>(XBeeRequest request, int timeout) where T : XBeeResponse
        {
            return (T) Execute(request, timeout);
        }
#endif

        /// <summary>
        /// Sends a XBeeRequest and waits 1000 msec for the XBeeResponse
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        /// <exception cref="System.TimoutException">Throws an TimoutException when response could not be read.</exception>
        public XBeeResponse Execute(XBeeRequest request)
        {
            return Execute(request, 3000);
        }

        /// <summary>
        /// Sends a XBeeRequest and waits for the XBeeResponse for specified milliseconds
        /// </summary>
        /// <param name="request">The XBeeRequest.</param>
        /// <param name="timeout">Milliseconds to wait for the response.</param>
        /// <returns></returns>
        public XBeeResponse Execute(XBeeRequest request, int timeout)
        {
            lock (_syncLock)
            {
                _waitResponse = true;

                ExecuteNonQuery(request);

                if (_apiType == ApiType.Enabled || _apiType == ApiType.EnabledWithEscaped)
                {
                    int c = 0;

                    while (_waitResponse && ++c <= (timeout/10))
                    {
                        Thread.Sleep(10);
                    }

                    if (c > (timeout/10))
                    {
#if(MF)
				    throw new Exception("Could not receive response.");
#else
                        throw new TimeoutException("Could not receive response.");
#endif
                    }

                    if (_waitResponse)
                        return null;

                    OnFrameReceived(_receivedPacket);

                    return _receivedPacket;
                }

                if (_apiType == ApiType.Disabled)
                {
                    if (ReadResponse() == "OK")
                        return null;
                }

                throw new NotSupportedException("This ApiType is not supported.");
            }
        }

        public bool Ping()
        {
            try
            {
                var cmd = new DeviceTypeIdentifierCommand();
                XBeeResponse rsp = Execute(cmd);
                return rsp != null;
            }
            catch (TimeoutException)
            {
                return false;
            }
        }

        #endregion

        #region Private Methods

        //private void ReceiveData()
        private void SerialPortDataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                int bytesToRead = SerialPort.BytesToRead;

                //while (!_stopThd)
                {
                    if (bytesToRead == 0)
                    {
                        Thread.Sleep(20);
                    }
                    else
                    {
                        var bytes = new byte[1024]; // TODO: what is the maximum size of Zigbee packets?

                        int bytesRead = SerialPort.Read(bytes, 0, bytesToRead);

                        for (int i = 0; i < bytesRead; i++)
                        {
                            if (_apiType == ApiType.EnabledWithEscaped && XBeePacket.IsSpecialByte(bytes[i]))
                            {
                                if (bytes[i] == XBeePacket.PACKET_STARTBYTE)
                                {
                                    _readBuffer.WriteByte(bytes[i]);
                                }
                                else if (bytes[i] == XBeePacket.PACKET_ESCAPE)
                                {
                                    _readBuffer.WriteByte((byte) (0x20 ^ bytes[++i]));
                                }
                                else
                                    throw new Exception("This special byte should not appear.");
                            }
                            else
                                _readBuffer.WriteByte(bytes[i]);
                        }

                        bool startOk;
                        bool lengthAndCrcOk;

                        do
                        {
                            _readBuffer.Position = 0;

                            // startOK should be always true if there is at least on byte
                            startOk = ((byte) _readBuffer.ReadByte() == XBeePacket.PACKET_STARTBYTE);

                            if (!startOk)
                            {
                                bytes = _readBuffer.ToArray();
                                _readBuffer = new MemoryStream();

                                for (int i = 0; i < bytes.Length; i++)
                                {
                                    if (!startOk && bytes[i] != XBeePacket.PACKET_STARTBYTE)
                                        continue;

                                    startOk = true;
                                    _readBuffer.Write(bytes, i, bytes.Length - i);
                                    _readBuffer.Position = 0;
                                }
                            }

                            lengthAndCrcOk = CheckLengthAndCrc();

                            bytes = _readBuffer.ToArray();
                            _readBuffer.SetLength(0);

                            var br = new ByteReader(bytes, ByteOrder.BigEndian);

#if(LOG && !MF && !WindowsCE)
                                Console.WriteLine("<<\t" + ByteUtil.PrintBytes(bytes, false));
#endif

                            if (startOk && lengthAndCrcOk)
                            {
                                br.ReadByte(); // start byte
                                short length = br.ReadInt16();

                                CheckFrame(length, br);

                                if (br.AvailableBytes > 1)
                                {
                                    br.ReadByte(); // checksum of current API message

                                    // ok, there are more bytes for an additional frame packet
                                    byte[] available = br.GetAvailableBytes();
                                    _readBuffer.Write(available, 0, available.Length);
                                }
                            }
                            else
                            {
                                _readBuffer.Write(bytes, 0, bytes.Length);
                            }
                        } while (startOk & lengthAndCrcOk & (_readBuffer.Length > 4));
                    }
                }
            }
#if(!MF)
            catch (ThreadAbortException ex)
            {
                OnLogEvent(LogEventType.ServerException, ex.ToString());

#if(LOG && !MF && !WindowsCE)
    // Display a message to the console.
                Console.WriteLine("{0} : DisplayMessage thread terminating - {1}",
                    DateTime.Now.ToString("HH:mm:ss.ffff"),
                    (string)ex.ExceptionState);
#endif
            }
#else
            catch(Exception ex)
            {
                OnLogEvent(LogEventType.ServerException, ex.ToString());
            }
#endif
        }

        private bool CheckLengthAndCrc()
        {
            // can't be to short
            if (_readBuffer.Length < 4)
                return false;

            int length = (_readBuffer.ReadByte() << 8) + _readBuffer.ReadByte();

            if ((length + 4) > _readBuffer.Length)
            {
                return false;
            }

            var checksum = new XBeeChecksum();
            for (int i = 0; i < length; i++)
            {
                checksum.AddByte((byte) _readBuffer.ReadByte());
            }

            checksum.Compute();
            bool result = checksum.Verify((byte) _readBuffer.ReadByte());

            return result;
        }

        private void CheckFrame(short length, ByteReader br)
        {
            var apiId = (XBeeApiType) br.Peek();
            XBeeResponse res = null;

            switch (apiId)
            {
                case XBeeApiType.AtCommandResponse:
                    res = new AtCommandResponse(length, br);
                    break;
                case XBeeApiType.RemoteAtCommandResponse:
                    res = new RemoteAtResponse(length, br);
                    break;
                case XBeeApiType.ModemStatus:
                    res = new ModemStatusResponse(length, br);
                    OnModemStatusChanged((res as ModemStatusResponse).ModemStatus);
                    break;

                case XBeeApiType.RxPacket16:
                    res = new RxResponse16(length, br);
                    break;
                case XBeeApiType.RxPacket64:
                    res = new RxResponse64(length, br);
                    break;
                case XBeeApiType.TxStatus:
                    res = new TxStatusResponse(length, br);
                    break;

                case XBeeApiType.NodeIdentificationIndicator:
                    res = new ZNetNodeIdentificationResponse(length, br);
                    break;
                case XBeeApiType.ZNetRxPacket:
                    res = new ZNetRxResponse(length, br);
                    break;
                case XBeeApiType.XBeeSensorReadIndicator:
                    res = new XBeeSensorRead(length, br);
                    break;
                case XBeeApiType.ZNetIODataSampleRxIndicator:
                    res = new ZNetRxIoSampleResponse(length, br);
                    break;
                case XBeeApiType.ZNetTxStatus:
                    res = new ZNetTxStatusResponse(length, br);
                    break;
                    //case XBeeApiType.ZNetTxRequest:
                    //    res = new ZNetRxResponseS2(length, br); // Znet Serie 2
                    //    break;

                    //case XBeeApiType.TxRequest64: // Znet Serie 1
                    //    res = new RxResponse64(length, br);
                    //    break;

                default:
                    break;
            }

            if (res != null)
            {
                if (_waitResponse)
                {
                    if (res is AtCommandResponse && (res as AtCommandResponse).FrameID != _frameId)
                        return;

                    _receivedPacket = res;
                    _waitResponse = false;
                }
                else
                {
                    OnFrameReceived(res);
                }
            }
        }

        #region Event Handling

        private void OnFrameReceived(XBeeResponse response)
        {
            FrameReceivedEventHandler handler = FrameReceived;

            if (handler != null)
                handler(this, new FrameReceivedEventArgs(response));
        }

        private void OnModemStatusChanged(ModemStatusType status)
        {
            ModemStatusChangedEventHandler handler = ModemStatusChanged;

            if (handler != null)
                handler(this, new ModemStatusChangedEventArgs(status));
        }

        private void OnLogEvent(LogEventType eventType, string message)
        {
            LogEventHandler handler = LogEvent;

            if (handler != null)
                handler(this, new LogEventArgs(eventType, message));
        }

        #endregion

        //private string ReadTo(string value)
        //{
        //    string textArrived = string.Empty;

        //    if (value == null)
        //        throw new ArgumentNullException();

        //    if (value.Length == 0)
        //        throw new ArgumentException();

        //    byte[] byteValue = Encoding.UTF8.GetBytes(value);
        //    int byteValueLen = byteValue.Length;

        //    bool flag = false;
        //    byte[] buffer = new byte[byteValueLen];

        //    do
        //    {
        //        int bytesRead;
        //        int bufferIndex = 0;
        //        Array.Clear(buffer, 0, buffer.Length);

        //        do
        //        {
        //            bytesRead = _serialPort.Read(buffer, bufferIndex, 1);
        //            bufferIndex += bytesRead;

        //            if (bytesRead <= 0)
        //            {
        //                return null;
        //            }
        //        }
        //        while ((bufferIndex < byteValueLen)
        //                && (buffer[bufferIndex - 1] != byteValue[byteValueLen - 1]));

        //        char[] charData = Encoding.UTF8.GetChars(buffer);

        //        for (int i = 0; i < charData.Length; i++)
        //            textArrived += charData[i];

        //        flag = true;

        //        if (textArrived.Length > 0)
        //        {
        //            for (int i = 1; i <= value.Length; i++)
        //            {
        //                if (value[value.Length - i] != textArrived[textArrived.Length - i])
        //                {
        //                    flag = false;
        //                    break;
        //                }
        //            }
        //        }

        //    } while (!flag);

        //    if (textArrived.Length >= value.Length)
        //        textArrived = textArrived.Substring(0, textArrived.Length - value.Length);

        //    return textArrived;
        //}

        protected void WriteCommand(string s)
        {
#if(LOG && !MF && !WindowsCE)
            Console.WriteLine(s);
#endif
            byte[] bytes = Encoding.UTF8.GetBytes(s);
            SerialPort.Write(bytes, 0, bytes.Length);
        }

        protected string ReadResponse()
        {
            string s = SerialPort.ReadTo("\r");

#if(LOG && !MF && !WindowsCE)
            Console.WriteLine(s);
#endif

            return s;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Close();

            if (SerialPort != null)
            {
                SerialPort.Dispose();
                SerialPort = null;
            }
        }

        #endregion

        #region Obsolete Members

        [Obsolete("Use XBee.ExecuteNonQuery(XBeeRequest) instead.", true)]
        public bool SendPacket(XBeePacket packet)
        {
            throw new NotSupportedException("This method is not supported any more.");
        }

        [Obsolete("Use XBee.Execute(XBeeRequest) instead.", true)]
        public XBeeResponse SendCommand(AtCommand cmd)
        {
            throw new NotSupportedException("This method is not supported any more.");
        }

        #endregion
    }
}